import os
import re

import requests

from requests.auth import HTTPBasicAuth
from threading import Thread
from typing import Any, List, Callable, Union, Tuple, IO, Pattern
from script.server.teleweb.TelewebServer import TelewebServer
from script.server.teleweb.FileNode import FileNode


class _DownloadContext(object):
    def __init__(self, user_on_progress: Callable[[str, int, int], None]):
        self.file: Union[None, IO] = None
        self.curr_size: int = 0
        self.total_size: int = 100
        self.on_progress: Callable[[str, int, int], None] = user_on_progress

    def notify(self, title: str):
        if self.on_progress:
            self.on_progress(title, self.curr_size, self.total_size)


class HttpTelewebServer(TelewebServer):
    def __init__(self, host: str, pattern_line: str, pattern_info: str,
                 username: Union[None, str] = None, password: Union[None, str] = None):
        self._host = host
        self._pattern_line: Pattern = re.compile(pattern_line)
        self._pattern_info: Pattern = re.compile(pattern_info)
        self._username = username
        self._password = password
        if self._username:
            self._auth: HTTPBasicAuth = HTTPBasicAuth(self._username, self._password)
        else:
            self._auth = None

        # create root node
        self._root = FileNode(None, self._host, '/', '<N/A>', FileNode.FILE_TYPE_DIR, self._auth,
                              self._pattern_line, self._pattern_info)
        self._root.parent = self._root

        # load root node
        self._root.child('.')

        # set pwd to roots
        self._pwd = self._root

        self._push_pop = []

    def name(self) -> str:
        pass

    def ls(self) -> Tuple[List[str], List[str]]:
        return self._pwd.ls()

    def ls_info(self) -> List[Tuple[str, str, str, str]]:
        nodes, _ = self._pwd.list()
        return [(node.name, node.file_type, node.date, node.pwd) for node in nodes]

    def cd(self, path: Union[str, int]) -> bool:
        if isinstance(path, str):
            pwd = self._pwd
            path = path.strip()
            if path.startswith('/'):
                pwd = self._root
            path = path.strip('[ /]')

            parts = path.split('/')
            for file in parts:
                pwd = pwd.child(file)
                if pwd is None or not pwd.is_dir():
                    return False

            self._pwd = pwd
            self._pwd.refresh(False)
            return True
        elif isinstance(path, int):
            pwd = self._pwd.child(path)
            if pwd is None or not pwd.is_dir():
                return False
            self._pwd = pwd
            self._pwd.refresh(False)
            return True
        else:
            return False

    def pwd(self) -> str:
        return self._pwd.pwd()

    def is_sw_like(self, path: str = None) -> bool:
        if path is None:
            return self._pwd.is_sw_like()
        else:
            node = self._find_node(path)
            return False if node is None else node.is_sw_like()

    def download_dir(self, local_path: str, remote_path: Union[str, None],
                     download_sub_dir: bool = True,
                     on_progress: Callable[[str, int, int], None] = None):
        self.pushd()

        if remote_path is None:
            remote_path = self._pwd.pwd()

        if self.cd(remote_path):
            if on_progress is None:
                def default_on_progress(path: str, cur_progress: int, max_progress: int, params: Any):
                    print('not implemented')

                on_progress = default_on_progress

            # create context
            context = _DownloadContext(on_progress)

            # notify begin
            context.notify(TelewebServer.PROGRESS_BEGIN)

            # get download list
            download_list = self._get_file_list(self._pwd, download_sub_dir)

            # get total download size
            context.total_size = 0
            for file in download_list:
                context.total_size += file.download_size()

            # start download in thread
            if context.total_size > 0:
                thread = Thread(target=lambda: self._download_dir_internal(local_path.replace('\\', '/'),
                                                                           download_list,
                                                                           context))
                thread.start()
                thread.join()
            else:
                context.total_size = 100

            # notify end
            context.notify(TelewebServer.PROGRESS_END)

        self.popd()

    def _download_dir_internal(self, local_path: str, files: List[FileNode], context: _DownloadContext):
        for file in files:
            file_path = '%s%s' % (local_path, file.pwd())
            HttpTelewebServer._prepare_dir(file_path)
            with open(file_path, 'wb') as download_file:
                # send request
                with requests.get(file.url(), verify=False, auth=self._auth, stream=True) as response:
                    # do download
                    for chunk in response.iter_content(file.DOWNLOAD_BUFFER_SIZE):
                        context.curr_size += len(chunk)
                        download_file.write(chunk)
                        context.notify(file.name)

    def _get_file_list(self, node: FileNode, download_sub_dir=True) -> List[FileNode]:
        download_list = []

        if download_sub_dir:
            if node.is_dir():
                nodes, _ = node.list()
                for n in nodes:
                    download_list += self._get_file_list(n)
            else:
                download_list.append(node)
        else:
            if node.is_dir():
                nodes, _ = node.list()
                for n in nodes:
                    if not n.is_dir():
                        download_list.append(n)
            else:
                download_list.append(node)

        return download_list

    def download_file(self, remote_path: str, on_progress: Callable[[str, int, int], None] = None) -> bytes:
        content = b''

        node = self._find_node(remote_path)
        if node is None:
            return content

        # notify begin
        if on_progress:
            on_progress(TelewebServer.PROGRESS_BEGIN, 0, 100)

        # send request
        curr_size: int = 0
        total_size: int = 0
        with requests.get(node.url(), verify=False, auth=self._auth, stream=True) as response:
            if 'content-length' in response.headers:
                file_size = response.headers['content-length'].strip()
                if file_size == '':
                    total_size = -1
                else:
                    total_size = int(file_size)
            else:
                total_size = -1  # size unknown

            # do download
            for chunk in response.iter_content(node.DOWNLOAD_BUFFER_SIZE):
                curr_size += len(chunk)
                content.join(chunk)
                if on_progress:
                    on_progress(node.pwd(), curr_size, total_size)

        if on_progress:
            on_progress(TelewebServer.PROGRESS_END, curr_size, total_size)

    def get_name(self, path: str) -> str:
        node = self._find_node(path)
        return None if node is None else node.name

    def get_date(self, path: str) -> str:
        node = self._find_node(path)
        return None if node is None else node.date

    def get_type(self, path: str) -> str:
        node = self._find_node(path)
        return None if node is None else node.file_type

    def get_info(self, path: str) -> Tuple[str, str, str]:
        node = self._find_node(path)
        if node:
            return node.name, node.date, node.file_type
        else:
            return '<Unknown>', '<Unknown>', '<Unknown>'

    def pushd(self):
        self._push_pop.append(self._pwd)

    def popd(self):
        self._pwd = self._push_pop[-1]
        self._push_pop = self._push_pop[0:-1]

    def _find_node(self, path: str) -> FileNode:
        self.pushd()

        node: Union[None, FileNode] = None
        if self.cd(path):
            node = self._pwd

        self.popd()
        return node

    @staticmethod
    def _prepare_dir(path: str):
        parts = path.split('/')
        if len(parts) < 2:
            return

        d = '/'.join(parts[0:-1])
        os.makedirs(d, exist_ok=True)
